'use strict';

const path  = require('path');
const crc16 = require('crc').crc16xmodem;
const utils = require(path.join(__dirname, '..', 'helpers', 'utils.js'));
const proParams = require('./params.js');

/*
 * Frame Definition
 * +-------------+-----+-----+-----+-----+-----------+------+----------+-----+-----------+
 * | kFrameStart | MAC | LEN | CMD | RES | VALID_LEN | Data | FillData | CRC | kFrameEnd |
 * |   1 byte    |  8  |  4  |  2  |  1  |  4 bytes  |      |          |  2  | 1 byte    | 
 * +-------------+-----+-----+-----+-----+-----------+------+----------+-----+-----------+
*/

function _verify_exist(v) {
  return typeof v !== 'undefined' && v !== null;
}

function _init_frame(data) {
  this._frame.writeUInt8(proParams.FRAME_VALUES.kFrameStart, 0);
  _fill_mac.call(this);
  _fill_cmd.call(this);
  _fill_res.call(this);
  _fill_len.call(this);
  data.copy(this._frame, 20);
  _fill_crc.call(this);
  this._frame.writeUInt8(proParams.FRAME_VALUES.kFrameEnd, this._dataLen+22);
}

function _fill_mac() {
  this._frame.writeUInt32BE(parseInt(this._mac.substring(0, 8), 16), 1);
  this._frame.writeUInt32BE(parseInt(this._mac.substring(8, 16), 16), 5);
}

function _fill_cmd() {
  this._frame.writeUInt16BE(this._cmd, 13);
}

function _fill_res() {
  this._frame.writeUInt8(this._res, 15);
}

function _fill_len() {
  this._frame.writeUInt32BE(this._dataLen+7, 9);
  this._frame.writeUInt32BE(this._dataLen, 16);    
}

function _fill_crc() {    
  this._frame.writeUInt16BE(_get_crc.call(this), this._dataLen+20);
}

function _get_crc() {
  let buf = this._frame.slice(0, this._dataLen+20);
  return crc16(buf);
}

class dataFrame {
  /**
   * @description Constructor of protocol frame
   * @param {object} options - init values
   * @param {int} options.cmd - cmd with 2bytes
   * @param {int} options.res - response with 1 byte
   * @param {string} options.mac - mac address 8 bytes with hex string format
   * @param {Array} options.data - data field 
   * @returns {object}
   */
  constructor(options) {
    this._cmd = _verify_exist(options.cmd) ? options.cmd : 0x00;
    this._res = _verify_exist(options.res) ? options.res : 0xFF;
    this._mac = _verify_exist(options.mac) ? 
      utils.pad(proParams.DEVICE_VALUES.MAC_PADDING, options.mac, true) : 'FFFFFFFFFFFFFFFF';
    this._dataLen = options.data ? options.data.length : 1024;
    this._frame = Buffer.allocUnsafe(proParams.FRAME_VALUES.kFrameFixedSize+this._dataLen);
    _init_frame.call(this, options.data);
  }

  /**
   * @description get the whole frame data
   * @returns {Buffer}
   */
  frame() {
    return this._frame;
  }

  /**
   * @description Get/Set mac field
   * @param {string} mac - optional mac address 8 bytes with hex string format
   * @returns {string}
   */
  mac(mac) {
    if (_verify_exist(mac)) {
      this._mac = mac;
      _fill_mac.call(this);
      _fill_crc.call(this);
    }
    return this._mac.toUpperCase();
  }

  /**
   * @description Get/Set data field
   * @param {Array} data - optional data field
   * @returns {Buffer}
   */
  data(data) {
    if (_verify_exist(data)) {
      if (data.length === this._dataLen) {
        data.copy(this._frame, 20);
        _fill_crc.call(this);
      } else {
        this._dataLen = data.length;
        this._frame = Buffer.allocUnsafe(proParams.FRAME_VALUES.kFrameFixedSize+this._dataLen);
        _init_frame.call(this, data);
      }
    }
    return this._frame.slice(proParams.FRAME_VALUES.kFrameHeaderSize, proParams.FRAME_VALUES.kFrameHeaderSize+this._dataLen);
  }

  /**
   * @description Get/Set cmd field
   * @param {int} cmd - optional cmd field
   * @returns {int}
   */
  cmd(cmd) {
    if (_verify_exist(cmd)) {
      this._cmd = cmd;
      _fill_cmd.call(this);
      _fill_crc.call(this);
    }
    return this._cmd;
  }

  /**
   * @description Get/Set res field
   * @param {int} res - optional res field
   * @returns {int}
   */
  res(res) {
    if (_verify_exist(res)) {
      this._res = res;
      _fill_res.call(this);
      _fill_crc.call(this);
    }
    return this._res;
  }
}

/**
 * @description Verify data is a valid frame
 * @param {Buffer} data - data to verify
 * @param {int} offset - start position, default: 0
 * @returns {int} - frame end position with input
 * @returns -- 0: not a full frame package ; -1: invalid frame package
 */
function verifyFrame(data, offset) {
  let start = offset ? offset : 0;
  // minimum size of frame
  if ((data.length-start) < proParams.FRAME_VALUES.kFrameFixedSize) {
    return -1
  };

  // verify start
  if (data[start] !== proParams.FRAME_VALUES.kFrameStart) {
    return 0;
  }

  // verify data length
  let len = data.readUInt32BE(start+9);
  let valid_len = data.readUInt32BE(start+16)
  if ((len-valid_len) !== 7) {
    return 0;
  }

  if ((data.length-start-16) < len) {
    return -1;
  }

  // verify end
  let end = start + 13 + len;
  if (data[end+2] !== proParams.FRAME_VALUES.kFrameEnd) {
    return 0;
  }

  // verify crc
  let buf = data.slice(start, end);
  if (crc16(buf) != data.readUInt16BE(end)) {
    return 0;
  }
  return end+3;
}


/**
 * @description find the next frame start postion
 * @param {Buffer} data - data to retrieve
 * @param {int} offset - start position, default: 0
 * @returns {int}
 */
function _nextFrameStart(data, offset) {
  while (offset < data.length) {
    if (data[offset] === proParams.FRAME_VALUES.kFrameStart) {
      break;
    }
    ++offset;
  }
  return offset;
}

/**
 * @description get the frame from buffer
 * @param {Buffer} data - data to retrieve
 * @returns {Array} - [dataFrame, start, end]
 */
function retrieveFrame(data) {
  let start = _nextFrameStart(data, 0);
  console.log(start);
  let end = verifyFrame(data, start);
  if (end === -1) {
    return [null, start, end];
  } else if (end === 0) {
    return [null, start+1, end];
  }

  let options = {
    mac: utils.pad('00000000', data.readUInt32BE(start+1).toString(16), true) 
            + utils.pad('00000000', data.readUInt32BE(start+5).toString(16), true),
    cmd: data.readUInt16BE(start+13),
    res: data.readUInt8(start+15)
  };

  let len = data.readUInt32BE(start+9);
  let buf = data.slice(start+proParams.FRAME_VALUES.kFrameHeaderSize, start+13+len);
  options.data = buf;
  return [new dataFrame(options), start, end];
}

/**
 * @description generate the device config file data
 * @param {Array} devices - device objects
 * @param {string} devices.item.devMac - device mac address
 * @param {string} devices.item.group_mask - group mask
 * @param {int} devices.item.brightness_min - min brightness
 * @param {int} devices.item.brightness_max - max brightness
 * @param {int} devices.item.brightness_power_on - power on brightness
 * @param {int} devices.item.brightness_fault - fault brightness
 * @param {int} devices.item.coefficiency - coefficiency
 * @param {int} devices.item.transition_duration - transition duration time
 * @returns {Buffer}
 */
function generateDevCfgData(devices) {
  let data_size = devices.length * proParams.FILEDATA_VALUES.kDevAttrSize + proParams.FILEDATA_VALUES.kHeadSize;
  let buf = Buffer.alloc(data_size + 1024 - (data_size % 1024));
  
  // fill the file header
  buf[0] = proParams.FILEDATA_VALUES.kFileTag_CFG;
  buf[1] = proParams.FILEDATA_VALUES.kFileVer;
  buf[2] = proParams.FILEDATA_VALUES.kDevAttrSize;
  buf.writeUInt16BE(devices.length, 3);
  let pos = 8;
  devices.forEach((item)=> {
    // mac address - 8 bytes
    buf.writeUInt32BE(parseInt(item.devMac.substring(0, 8), 16), pos);
    buf.writeUInt32BE(parseInt(item.devMac.substring(8, 16), 16), pos+4);
    pos += 8;

    // group mask - 8 bytes
    buf.writeUInt32BE(parseInt(item.group_mask.substring(0, 8), 16), pos);
    buf.writeUInt32BE(parseInt(item.group_mask.substring(8, 16), 16), pos+4);
    pos += 8;

    // brightness min - 4 bytes
    buf.writeUInt32BE(item.brightness_min, pos);
    pos += 4;

    // brightness max - 4 bytes
    buf.writeUInt32BE(item.brightness_max, pos);
    pos += 4;
    
    // brightness power on - 4 bytes
    buf.writeUInt32BE(item.brightness_power_on, pos);
    pos += 4;

    // brightness fault - 4 bytes
    buf.writeUInt32BE(item.brightness_fault, pos);
    pos += 4;

    // coefficiency - 4 bytes
    buf.writeUInt32BE(item.coefficiency, pos);
    pos += 4;
    
    // transition_duration - 1 byte
    buf.writeUInt8(item.transition_duration, pos);
    pos += 1;    
  });

  // crc in file header
  let data = buf.slice(proParams.FILEDATA_VALUES.kHeadSize, data_size);
  buf.writeUInt16BE(crc16(data), (proParams.FILEDATA_VALUES.kHeadSize-3));

  // check sum
  let sum_check = 0;
  for (let i=0; i<(proParams.FILEDATA_VALUES.kHeadSize-1); ++i) {
    sum_check += buf[i];
  }
  buf.writeUInt8((sum_check & 0xFF), (proParams.FILEDATA_VALUES.kHeadSize-1));

  return buf;
}


/**
 * @description generate the shedule config file data
 * @param {Array} schedules - schedule objects
 * @param {int} schedules.item.mark - reserved
 * @param {int} schedules.item.s_month - start month
 * @param {int} schedules.item.s_day - start day
 * @param {int} schedules.item.e_month - end month
 * @param {int} schedules.item.e_day - end day
 * @param {int} schedules.item.hour - hour
 * @param {int} schedules.item.minute - minute
 * @param {int} schedules.item.second - second
 * @param {int} schedules.item.addr - group address
 * @param {enum} schedules.item.switch - on; off
 * @param {int} schedules.item.brightness - brightness values 
 * @returns {Buffer}
 */
function generateScheCfgData(schedules) {
  let data_size = schedules.length * proParams.FILEDATA_VALUES.kScheAttrSize + proParams.FILEDATA_VALUES.kHeadSize;
  let buf = Buffer.alloc(data_size + 1024 - (data_size % 1024));
  // fill the file header
  buf[0] = proParams.FILEDATA_VALUES.kFileTag_SCH;
  buf[1] = proParams.FILEDATA_VALUES.kScheFileVer;
  buf[2] = proParams.FILEDATA_VALUES.kScheAttrSize;
  buf.writeUInt16BE(schedules.length, 3);
  let pos = 8;
  schedules.forEach((item)=> {
    // schedule mark
    buf.writeUInt8((item.mark & 0xFF), pos++);

    // start month & day
    buf.writeUInt8((item.s_month & 0xFF), pos++);
    buf.writeUInt8((item.s_day & 0xFF), pos++);
    
    // end month & day
    buf.writeUInt8((item.e_month & 0xFF), pos++);
    buf.writeUInt8((item.e_day & 0xFF), pos++);

    // time
    buf.writeUInt8((item.hour & 0xFF), pos++);
    buf.writeUInt8((item.minute & 0xFF), pos++);
    buf.writeUInt8((item.second & 0xFF), pos++);

    // group addr
    buf.writeUInt8((item.group & 0xFF), pos++);

    // switch
    let sw_val = 0x00;
    if (item.switch === 'on') {
      sw_val = 0x01;
    }
    buf.writeUInt8(sw_val, pos++);

    // brightness
    buf.writeUInt32BE(item.brightness, pos);
    pos += 4;
  });

  // crc in file header
  let data = buf.slice(proParams.FILEDATA_VALUES.kHeadSize, data_size);
  buf.writeUInt16BE(crc16(data), (proParams.FILEDATA_VALUES.kHeadSize-3));

  // check sum
  let sum_check = 0;
  for (let i=0; i<(proParams.FILEDATA_VALUES.kHeadSize-1); ++i) {
    sum_check += buf[i];
  }
  buf.writeUInt8((sum_check & 0xFF), (proParams.FILEDATA_VALUES.kHeadSize-1));

  return buf;
}

module.exports.DataFrame          = dataFrame;
module.exports.verifyFrame        = verifyFrame;
module.exports.retrieveFrame      = retrieveFrame;
module.exports.generateDevCfgData = generateDevCfgData;
module.exports.generateScheCfgData = generateScheCfgData;